#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import os
import re

import pexpect

from common.common import execute_cmd
from common.common_models import Copy
from common.const import AuthType, CMDResult, RpcToolInterface, ParamConstant, CopyDataTypeEnum
from common.exception.common_exception import ErrCodeException
from common.job_const import ParamKeyConst
from common.number_const import NumberConst
from common.parse_parafile import get_env_variable
from common.util.check_utils import check_repo_path, is_valid_uuid
from common.util.cmd_utils import cmd_format
from common.util.common_utils import exec_rpc_tool_cmd, exec_rpc_tool_cmd_with_output
from common.util.exec_utils import check_path_valid, exec_overwrite_file
from gaussdbt.single import LOGGER
from gaussdbt.single.comm.gst_single_const import GstSingleKeyConst, GstSingleConst
from gaussdbt.single.comm.gst_single_error_code import ErrorCode
from gaussdbt.single.comm.utils.gst_single_comm_utils import GstSingleCommonUtil
from gaussdbt.single.comm.utils.gst_single_param_utils import GstSingleParamUtil


class GstSingleBackupUtil:
    @staticmethod
    def get_enable_sysdba_login_info(pid, param_dict, is_restore=False):
        auth_type = None
        if is_restore:
            auth_type = GstSingleParamUtil.get_sys_auth_type_for_restore(param_dict)
        else:
            auth_type = GstSingleParamUtil.get_sys_auth_type_for_backup(param_dict)
        # 启用免密登录
        enable_sysdba_login = (str(auth_type) != str(AuthType.APP_PASSWORD.value))
        sys_pwd = None
        if not enable_sysdba_login:
            if is_restore:
                sys_pwd = get_env_variable(f"{GstSingleKeyConst.TARGET_OBJ_SYS_AUTH_PWD}_{pid}")
            else:
                sys_pwd = get_env_variable(f"{GstSingleKeyConst.PROTECT_OBJ_SYS_AUTH_PWD}_{pid}")
        return enable_sysdba_login, sys_pwd

    @staticmethod
    def report_copy_info_by_rpc_tool(job_id, copy_info: Copy, cache_path):
        """使用rpctool工具上报副本信息"""
        input_path = os.path.realpath(os.path.join(cache_path, f"copy_info_in_{job_id}"))
        output_path = os.path.realpath(os.path.join(cache_path, f"copy_info_out_{job_id}"))
        params = dict()
        params["copy"] = copy_info.dict(by_alias=True)
        params["jobId"] = job_id
        try:
            exec_overwrite_file(input_path, params)
            if not exec_rpc_tool_cmd(RpcToolInterface.REPORT_COPY_INFO, input_path, output_path, LOGGER):
                err_msg = "Report backup copy information failed"
                raise ErrCodeException(ErrorCode.FAILED_EXECUTE_COMMAND,
                                       *[f"{ParamConstant.BIN_PATH}/rpctool.sh ReportCopyAdditionalInfo", err_msg],
                                       message=err_msg)
        finally:
            for tmp_path in (input_path, output_path):
                if os.path.isfile(tmp_path) and check_path_valid(tmp_path):
                    LOGGER.info("Remove temp copy info file: %s.", tmp_path)
                    os.remove(tmp_path)

    @staticmethod
    def get_backup_time_info(os_user, backup_dir, enable_sysdba_login=True, sys_pwd=None) -> dict:
        """获取备份时间信息"""
        if not check_repo_path(backup_dir):
            LOGGER.error("The backup directory(%s) is invalid.", backup_dir)
            raise ErrCodeException(ErrorCode.PARAMS_IS_INVALID, message="The backup directory is invalid.")
        query_bak_set_sql = cmd_format("SELECT START_TIME, COMPLETION_TIME FROM SYS_BACKUP_SETS WHERE DIR=\'{}\';",
                                       backup_dir)
        out = GstSingleCommonUtil.exec_gsdb_sql(
            os_user, query_bak_set_sql, enable_sysdba_login=enable_sysdba_login, sys_pwd=sys_pwd)
        for line in out.split("\n"):
            find_rets = re.findall(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', str(line))
            if find_rets and len(find_rets) == 2:
                bak_info_dict = dict()
                bak_info_dict[ParamKeyConst.COPY_BAK_TIME] = GstSingleCommonUtil.convert_backup_time_to_timestamp(
                    find_rets[1])
                bak_info_dict[GstSingleKeyConst.BAK_SET_START_TIME] = find_rets[0]
                bak_info_dict[GstSingleKeyConst.BAK_SET_COMPLETION_TIME] = find_rets[1]
                return bak_info_dict
        raise ErrCodeException(ErrorCode.BACKUP_DB_FAIL, message="Get completion time of full backup failed.")

    @staticmethod
    def get_gsdb_now_time(os_user, enable_sysdba_login=True, sys_pwd=None) -> str:
        """
        获取数据库当前时间
        :param os_user: 操作系统用户名
        :param enable_sysdba_login: 启用SYS免密登录
        :param sys_pwd: SYS密码
        :return: 当前时间
        """
        query_now_sql = "SELECT NOW();"
        out = GstSingleCommonUtil.exec_gsdb_sql(
            os_user, query_now_sql, enable_sysdba_login=enable_sysdba_login, sys_pwd=sys_pwd)
        for line in out.split("\n"):
            match_ret = re.match(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', str(line))
            if match_ret:
                return match_ret.group()
        raise ErrCodeException(ErrorCode.FAILED_EXECUTE_COMMAND, *[query_now_sql, out],
                               message="Query database now time failed.")

    @staticmethod
    def query_pre_copy_info_by_rpc_tool(app_info, job_id, sub_jod_id, cache_path) -> dict:
        """使用rpctool工具查询上一个副本信息"""
        LOGGER.info(f"Start querying previous copy info by rpctool, job id: %s, sub job id: %s.",
                    job_id, sub_jod_id)
        if not check_repo_path(cache_path) or not is_valid_uuid(sub_jod_id):
            raise ErrCodeException(ErrorCode.PARAMS_IS_INVALID, "The cache repository path is invalid.")
        input_path = os.path.join(cache_path, f"pre_copy_in_{sub_jod_id}")
        output_path = os.path.join(cache_path, f"pre_copy_out_{sub_jod_id}")
        for tmp_path in (input_path, output_path):
            if os.path.exists(tmp_path) and check_path_valid(tmp_path):
                LOGGER.info(f"Remove temp pre copy info file(%s) before querying previous copy.", tmp_path)
                os.remove(tmp_path)
        param_dict = dict()
        param_dict["application"] = app_info
        param_dict["types"] = [CopyDataTypeEnum.FULL_COPY.value, CopyDataTypeEnum.LOG_COPY.value]
        param_dict["copyId"] = job_id
        param_dict["jobId"] = job_id
        try:
            exec_overwrite_file(input_path, param_dict)
            ret, copy_dict = exec_rpc_tool_cmd_with_output(
                RpcToolInterface.QUERY_PREVIOUS_COPY, input_path, output_path, LOGGER)
            if not ret:
                raise ErrCodeException(ErrorCode.FAILED_EXECUTE_COMMAND, *[], message="")
            copy_msg = f"copy type: {copy_dict.get('type')}, id: {copy_dict.get('id')}, " \
                       f"timestamp: {copy_dict.get('timestamp')}"
            LOGGER.info("Query previous copy info by rpctool complete, %s, job id: %s, sub job id: %s.",
                        copy_msg, job_id, sub_jod_id)
            return copy_dict
        finally:
            for tmp_path in (input_path, output_path):
                if os.path.exists(tmp_path) and check_path_valid(tmp_path):
                    LOGGER.info("Remove temp copy info file(%s) after querying previous copy.", tmp_path)
                    os.remove(tmp_path)

    @staticmethod
    def get_depend_copy_time_info(app_info, job_id, sub_job_id, cache_path):
        """获取依赖副本的时间信息"""
        pre_copy_info = GstSingleBackupUtil.query_pre_copy_info_by_rpc_tool(
            app_info, job_id, sub_job_id, cache_path)
        pre_copy_type = pre_copy_info.get('type')
        pre_copy_ext_info = pre_copy_info.get('extendInfo', {})
        depend_copy_info = dict()
        if pre_copy_type == CopyDataTypeEnum.LOG_COPY.value:
            depend_copy_info[GstSingleKeyConst.LOG_BAK_START_TIME] = pre_copy_ext_info.get(
                GstSingleKeyConst.LOG_BAK_START_TIME)
            depend_copy_info[GstSingleKeyConst.PRE_COPY_END_TIMESTAMP] = pre_copy_ext_info.get(
                ParamKeyConst.COPY_END_TIME)
        else:
            depend_copy_info[GstSingleKeyConst.LOG_BAK_START_TIME] = pre_copy_ext_info.get(
                GstSingleKeyConst.BAK_SET_COMPLETION_TIME)
            depend_copy_info[GstSingleKeyConst.PRE_COPY_END_TIMESTAMP] = pre_copy_ext_info.get(
                ParamKeyConst.COPY_BAK_TIME)
        return depend_copy_info

    @staticmethod
    def execute_instance_backup(backup_cmd, os_user, fail_raise=True):
        """执行不需要数据库密码的实例备份"""
        LOGGER.info("Start executing backup command: %s, os user: %s.", backup_cmd, os_user)
        cmd = f"su - {os_user} -c '{backup_cmd}'"
        return_code, out, err = execute_cmd(cmd)
        if return_code != CMDResult.SUCCESS.value:
            LOGGER.error(f"Execute backup command failed, return code: %s, out: %s, err: %s.", return_code, out, err)
            if fail_raise:
                raise ErrCodeException(ErrorCode.FAILED_EXECUTE_COMMAND, *[backup_cmd, out],
                                       message="Execute backup command failed.")
            return False, str(out)
        LOGGER.info("Execute backup command success.")
        return True, str(out)

    @staticmethod
    def execute_instance_backup_with_pwd(backup_cmd, os_user, sys_pwd, fail_raise=True):
        """执行需要数据库密码的实例备份"""
        LOGGER.info("Start executing need auth backup command: %s, os_user: %s.", backup_cmd, os_user)
        su_cmd = f"su - {os_user}"
        ssh = pexpect.spawn(su_cmd, encoding='utf-8')
        try:
            index = ssh.expect(['[#\$>]', pexpect.EOF, pexpect.TIMEOUT], timeout=NumberConst.TEN)
            if index != 0:
                out = ssh.before
                LOGGER.error("Execute need auth backup change user failed, index: %s, out: %s.", index, out)
                raise ErrCodeException(ErrorCode.FAILED_EXECUTE_COMMAND, *[su_cmd, out])
            ssh.sendline(backup_cmd)
            index = ssh.expect(['Username:', pexpect.EOF, pexpect.TIMEOUT], timeout=NumberConst.TEN)
            if index != 0:
                out = ssh.before
                LOGGER.error("Execute need auth backup expect username failed, index: %s, output: %s.", index, out)
                raise ErrCodeException(ErrorCode.FAILED_EXECUTE_COMMAND, *[backup_cmd, out])
            ssh.sendline(GstSingleConst.SYS_DBA)
            index = ssh.expect(['Password:', pexpect.EOF, pexpect.TIMEOUT], timeout=NumberConst.TEN)
            if index != 0:
                out = ssh.before
                LOGGER.error("Execute need auth backup expect password failed, index: %s, out: %s.", index, out)
                raise ErrCodeException(ErrorCode.FAILED_EXECUTE_COMMAND, *[backup_cmd, out])
            ssh.sendline(sys_pwd)
            index = ssh.expect(['[#\$>]', pexpect.EOF, pexpect.TIMEOUT], timeout=GstSingleConst.BACKUP_TIMEOUT)
            if index != 0:
                out = ssh.before
                LOGGER.error("Execute need auth backup command failed, index: %s, out: %s.", index, out)
                raise ErrCodeException(ErrorCode.FAILED_EXECUTE_COMMAND, *[backup_cmd, out])
            chk_ret = str(ssh.before)
            if "Successfully operation" not in chk_ret:
                LOGGER.error("Execute need auth backup command failed, result: %s.", chk_ret)
                if fail_raise:
                    raise ErrCodeException(ErrorCode.FAILED_EXECUTE_COMMAND, *[backup_cmd, chk_ret],
                                           message="Execute need auth backup command failed.")
                return False, chk_ret
            LOGGER.info("Execute need auth backup command success.")
            return True, chk_ret
        finally:
            if ssh:
                ssh.close()
